package com.zlq.cn.service.un;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zlq.cn.cache.RedisCacheService;
import com.zlq.cn.cache.RedisKey;
import com.zlq.cn.cache.topic.RedisTopicService;
import com.zlq.cn.config.FinderConfiguration;
import com.zlq.cn.dto.AddressRemindDTO;
import com.zlq.cn.entity.BindUserLocation;
import com.zlq.cn.entity.LocationLog;
import com.zlq.cn.entity.LocationLogTest;
import com.zlq.cn.entity.User;
import com.zlq.cn.mapper.un.LocationLogMapper;
import com.zlq.cn.task.LocationLogSchedulerTask;
import com.zlq.cn.util.LocationUtils;
import com.zlq.cn.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author zhoucl
 * @since 2020-06-13
 */
@Service
public class LocationLogService extends ServiceImpl<LocationLogMapper, LocationLog> {
    private static int DEFAULT_BATCH_SIZE = 1000;
    private static final Map<String, List<LocationLog>> AWAIT_SAVE_LOCATION_LOG = new LinkedHashMap<>();// LinkedHashMap按顺序处理
    private static final List<LocationLog> TEMP_LOCATION_LOGS = new ArrayList<>();

    protected Logger logger = LoggerFactory.getLogger(getClass().getName());
    private boolean isExecComplete = true;
    @Resource
    private FinderConfiguration finderConfiguration;
    @Resource
    private UserService userService;
    @Resource
    private RedisCacheService redisCacheService;
    @Resource
    private LocationLogTestService locationLogTestService;
    @Resource
    private RedisTopicService redisTopicService;
    @Resource
    private BindUserLocationService bindUserLocationService;

    @PostConstruct
    private void init() {
        DEFAULT_BATCH_SIZE = finderConfiguration.getLocationLogBatchSize().intValue();
    }

    /**
     * 添加上报数据
     */
    public static void addLocationLog(LocationLog locationLog) {
        if (locationLog == null) {
            return;
        }
        if (TEMP_LOCATION_LOGS.size() >= DEFAULT_BATCH_SIZE) {
            synchronized (LocationLogSchedulerTask.class) {
                if (TEMP_LOCATION_LOGS.size() > DEFAULT_BATCH_SIZE) {
                    syncLocationLog();
                }
            }
        }
        TEMP_LOCATION_LOGS.add(locationLog);
    }

    public static void syncLocationLog() {
        if (TEMP_LOCATION_LOGS.size() > 0) {
            AWAIT_SAVE_LOCATION_LOG.put(StringUtils.createToken(), new ArrayList<>(TEMP_LOCATION_LOGS));
            TEMP_LOCATION_LOGS.clear();
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void saveLocationLog() {
        if (isExecComplete) {
            isExecComplete = false;
            if (AWAIT_SAVE_LOCATION_LOG.size() == 0) {
                syncLocationLog();
                isExecComplete = true;
                return;
            }
            long time = System.currentTimeMillis();
            // 存储有多少用户需要修改最新定位（定位比较特殊）
            Map<Integer, LocationLog> userNewLocationMap = new HashMap<>();
            // 存储有多少用户需要修改电量
            Map<Integer, LocationLog> userNewBatteryMap = new HashMap<>();
            // 存储有多少用户需要修改网络类型
            Map<Integer, LocationLog> userNewNetworkTypeMap = new HashMap<>();
            // 存储离开到达提醒
            Map<Integer, List<BindUserLocation>> bindUserLocationMap = new HashMap<>();
            List<LocationLog> locationLogs = new ArrayList<>();
            List<LocationLog> updateLocationLogs = new ArrayList<>();
            // 本次处理用户
            Map<Integer, User> execUserMap = new HashMap<>();
            // 用户上次处理的地址
            Map<String, LocationLog> oldLocationLogMap = new HashMap<>();
            Set<String> keySet = new HashSet<>(AWAIT_SAVE_LOCATION_LOG.keySet());
            logger.info("开始处理定位数据==>" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            try {
                for (String key : keySet) {
                    locationLogs.addAll(AWAIT_SAVE_LOCATION_LOG.get(key));
                }
                locationLogs.sort(LocationLogService::compare);
                for (LocationLog locationLog : locationLogs) {
                    // 查询上报用户最后一次上报位置信息
                    User user = execUserMap.get(locationLog.getUserId());
                    if (user == null) {
                        user = userService.getUserByIdCache(locationLog.getUserId());
                        // 用户不存在不处理本次数据
                        if (user == null) {
                            locationLog.setNeedRemove(true);
                            continue;
                        }
                        execUserMap.put(locationLog.getUserId(), user);
                    }
                    // 处理定位改变
                    // 位置没有发生改变需要查询出最后一次上报的信息修改停留时长，在一个地方停留时间很长基本都是修改历史数据
                    if (!user.isLocationChange() && user.getAddress() != null && user.getAddress().equals(locationLog.getAddress())) {
                        String oldLocationLogKey = String.format("%s@@%s", locationLog.getUserId(), locationLog.getAddress());
                        LocationLog oldLocationLog = oldLocationLogMap.get(oldLocationLogKey);
                        if (oldLocationLog == null) {
                            oldLocationLog = redisCacheService.get(RedisKey.createLastLocationLogKey(user.getId() + ""));
                            if (oldLocationLog == null) {
                                oldLocationLog = lambdaQuery().eq(LocationLog::getUserId, locationLog.getUserId())
                                        .eq(LocationLog::getAddress, locationLog.getAddress())
                                        .orderByDesc(LocationLog::getCreateTime)
                                        .last("limit 1").one();
                                if (oldLocationLog != null) {
                                    redisCacheService.set(RedisKey.createLastLocationLogKey(user.getId() + ""), oldLocationLog);
                                }
                            }
                            if (oldLocationLog != null) {
                                oldLocationLogMap.put(oldLocationLogKey, oldLocationLog);
                            }
                        }
                        // 没有上报过用户信息里面的经纬度也会为空基本不会出现null的情况
                        if (oldLocationLog != null) {
                            oldLocationLog.setStopTime(oldLocationLog.getCreateTime().until(locationLog.getCreateTime(), ChronoUnit.SECONDS));
                            oldLocationLog.setUpdateTime(locationLog.getCreateTime());
                            // 本次上报数据标记删除
                            locationLog.setNeedRemove(true);
                        }
                    } else {
                        // 更上一次处理位置改变新增定位信息，要注意修改上一次最后的停留时长
                        user.setLocationChange(true);
                        LocationLog tempLocationLog = userNewLocationMap.get(locationLog.getUserId());
                        // 本次处理还未创建
                        if (tempLocationLog == null) {
                            // 处理上一次停留时长，还未开始处理就修改最后一次上报位置
                            LocationLog cacheLocationLog = redisCacheService.get(RedisKey.createLastLocationLogKey(user.getId() + ""));
                            if (cacheLocationLog != null && cacheLocationLog.getId() != null && cacheLocationLog.getId() > 0) {
                                cacheLocationLog.setStopTime(cacheLocationLog.getCreateTime().until(locationLog.getCreateTime(), ChronoUnit.SECONDS));
                                cacheLocationLog.setUpdateTime(locationLog.getCreateTime());
                                updateLocationLogs.add(cacheLocationLog);
                            }
                            userNewLocationMap.put(locationLog.getUserId(), locationLog);
                        } else {
                            tempLocationLog.setStopTime(tempLocationLog.getCreateTime().until(locationLog.getCreateTime(), ChronoUnit.SECONDS));
                            tempLocationLog.setUpdateTime(locationLog.getCreateTime());
                            if (tempLocationLog.getAddress().equals(locationLog.getAddress())) {
                                // 两次经纬度相同时间重新计算，并且删除本次的定位信息
                                locationLog.setNeedRemove(true);
                            } else {
                                // 位置改变重置该用户最后定位信息
                                userNewLocationMap.put(locationLog.getUserId(), locationLog);
                            }
                        }
                    }
                    // 到达/离开提醒
                    // xxx已经到达了你的指定地点，快去看看他的历史路径把
                    // xxx已经离开了你的指定地点，快去看看他的历史路径把
                    List<BindUserLocation> bindUserLocations = bindUserLocationMap.get(locationLog.getUserId());
                    if (bindUserLocations == null) {
                        bindUserLocations = redisCacheService.get(RedisKey.createAddressRemind(locationLog.getUserId()));
                        if (bindUserLocations == null) {
                            bindUserLocations = bindUserLocationService.list(new LambdaQueryWrapper<BindUserLocation>()
                                    .eq(BindUserLocation::getToUserId, locationLog.getUserId()));
                            if (bindUserLocations == null) {
                                bindUserLocations = new ArrayList<>();
                            }
                        }
                        bindUserLocationMap.put(locationLog.getUserId(), bindUserLocations);
                    }
                    if (!CollectionUtils.isEmpty(bindUserLocations)) {
                        for (BindUserLocation bindUserLocation : bindUserLocations) {
                            float distance = LocationUtils.calculateLineDistance(locationLog.getLongitude(),
                                    locationLog.getLatitude(), bindUserLocation.getLongitude(),
                                    bindUserLocation.getLatitude());
                            if (distance < finderConfiguration.getLocationDistance()) {// 到达
                                if (!bindUserLocation.isArrivRemind()) {
                                    AddressRemindDTO addressRemindDTO = new AddressRemindDTO();
                                    addressRemindDTO.setUser(user);
                                    addressRemindDTO.setBindUserLocation(bindUserLocation);
                                    redisTopicService.aliPushAddressArriveConvertAndSend(addressRemindDTO);
                                }
                                bindUserLocation.setArrivRemind(true);
                                bindUserLocation.setLeaveRemind(false);
                            } else {// 离开
                                if (!bindUserLocation.isLeaveRemind()) {
                                    AddressRemindDTO addressRemindDTO = new AddressRemindDTO();
                                    addressRemindDTO.setUser(user);
                                    addressRemindDTO.setBindUserLocation(bindUserLocation);
                                    redisTopicService.aliPushAddressLeaveRemindConvertAndSend(addressRemindDTO);
                                }
                                bindUserLocation.setLeaveRemind(true);
                                bindUserLocation.setArrivRemind(false);
                            }
                        }
                    }
                    bindUserLocationMap.forEach((userId, bindUserLocations1) -> {
                        if (bindUserLocations1 != null)
                            redisCacheService.set(RedisKey.createAddressRemind(userId), bindUserLocations1);
                    });

                    // 电量改变
                    if (user.getBattery() != null && user.getBattery() != locationLog.getBattery()) {
                        userNewBatteryMap.put(locationLog.getUserId(), locationLog);
                    }
                    // 网络类型改变
                    if (!locationLog.getNetworkType().equals(user.getNetworkType())) {
                        userNewNetworkTypeMap.put(locationLog.getUserId(), locationLog);
                    }
                }
                try {
                    Map<Integer, LocationLog> distinctUserLocationMap = new HashMap<>();
                    distinctUserByCreateTime(distinctUserLocationMap, userNewLocationMap);
                    distinctUserByCreateTime(distinctUserLocationMap, userNewBatteryMap);
                    distinctUserByCreateTime(distinctUserLocationMap, userNewNetworkTypeMap);

                    // 拼装修改用户定位
                    List<User> modifyUsers = new ArrayList<>();
                    for (LocationLog locationLog : distinctUserLocationMap.values()) {
                        User modifyUser = new User();
                        modifyUser.setId(locationLog.getUserId());
                        modifyUser.setLatitude(locationLog.getLatitude());
                        modifyUser.setLongitude(locationLog.getLongitude());
                        modifyUser.setAddress(locationLog.getAddress());
                        modifyUser.setBattery(locationLog.getBattery());// 设置最新电量
                        modifyUser.setNetworkType(locationLog.getNetworkType());
                        modifyUsers.add(modifyUser);
                    }
                    logger.info("本次处理用户修改定位数据==>" + modifyUsers.size() + "条数据");
                    if (!modifyUsers.isEmpty()) {
                        modifyUsers.removeIf(modifyUser -> {// 过滤掉位置跟电量没有改变的数据
                            User userCache = execUserMap.get(modifyUser.getId());
                            if (userCache != null) {
                                return modifyUser.getAddress().equals(userCache.getAddress())
                                        && modifyUser.getBattery().equals(userCache.getBattery())
                                        && modifyUser.getNetworkType().equals(userCache.getNetworkType());
                            }
                            return false;
                        });
                        userService.updateBatchLocationLogInfo(modifyUsers, execUserMap);
                    }
                    modifyUsers.clear();
                } catch (Exception e1) {
                    logger.error("处理用户修改定位数据异常", e1);
                    throw e1;
                }
                logger.info("本次处理定位数据==>" + locationLogs.size() + "条数据");
                if (FinderConfiguration.DEV.equals(finderConfiguration.getProfile())) {// 测试环境保留一份全量数据
                    locationLogTestService.saveBatch(locationLogsTolocationLogTests(locationLogs));
                }
                locationLogs.removeIf(LocationLog::isNeedRemove);
                if (locationLogs.size() > 0) {
                    saveBatch(locationLogs);
                    // 插入之后才有id;位置有改变的用户重新设置最后定位
                    for (LocationLog locationLog : userNewLocationMap.values()) {
                        int index = locationLogs.indexOf(locationLog);
                        if (index >= 0) {
                            LocationLog tempLocationLog = locationLogs.get(index);
                            redisCacheService.set(RedisKey.createLastLocationLogKey(tempLocationLog.getUserId() + ""), tempLocationLog);
                        }
                    }
                }
                if (oldLocationLogMap.size() > 0) {
                    updateLocationLogs.addAll(oldLocationLogMap.values());
                }
                if (updateLocationLogs.size() > 0) {
                    updateBatchById(updateLocationLogs);
                }
            } catch (Exception e) {
                logger.error("处理定位上报数据异常", e);
                throw e;
            } finally {
                userNewLocationMap.clear();
                userNewBatteryMap.clear();
                userNewNetworkTypeMap.clear();
                bindUserLocationMap.clear();
                locationLogs.clear();
                for (String key : keySet) {
                    AWAIT_SAVE_LOCATION_LOG.remove(key);
                }
                keySet.clear();
                execUserMap.clear();
                oldLocationLogMap.clear();
                updateLocationLogs.clear();
                logger.info("本次处理时长==>" + (System.currentTimeMillis() - time) + "毫秒");
                isExecComplete = true;
            }
        }
    }

    /**
     * 按照时间正序
     */
    public static int compare(LocationLog o1, LocationLog o2) {
        if (o1.getCreateTime().isBefore(o2.getCreateTime())) {
            return -1;
        } else if (o1.getCreateTime().isAfter(o2.getCreateTime())) {
            return 1;
        }
        return 0;
    }

    private static List<LocationLogTest> locationLogsTolocationLogTests(List<LocationLog> locationLogs) {
        return locationLogs.stream().map(locationLog -> {
            LocationLogTest locationLogTest = new LocationLogTest();
            BeanUtils.copyProperties(locationLog, locationLogTest);
            return locationLogTest;
        }).collect(Collectors.toList());
    }

    /**
     * 整合有修改信息的用户，并按时间去重
     */
    private void distinctUserByCreateTime(Map<Integer, LocationLog> distinctUserLocationMap, Map<Integer, LocationLog> userNewLocationMap) {
        for (Map.Entry<Integer, LocationLog> item : userNewLocationMap.entrySet()) {
            LocationLog locationLog = distinctUserLocationMap.get(item.getKey());
            if (locationLog == null || locationLog.getCreateTime().isBefore(item.getValue().getCreateTime())) {
                distinctUserLocationMap.put(item.getKey(), item.getValue());
            }
        }
    }

    public static void main(String[] args) {
        List<Integer> re = new ArrayList<>();
        re.add(1);
        re.add(2);
        re.add(6);
        re.add(5);
        re.add(8);
        re.add(8);
        re.add(4);
        Collections.sort(re, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                if (o1 < o2) {
                    return -1;
                } else if (o1 > o2) {
                    return 1;
                }
                return 0;
            }
        });
        System.out.println(re);
    }
}
